/*
 * Copyright 2010 Impetus Infotech.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.impetus.kundera.db.accessor;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.persistence.PersistenceException;

import org.apache.cassandra.avro.Cassandra;
import org.apache.cassandra.thrift.Column;
import org.apache.cassandra.thrift.SuperColumn;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.impetus.kundera.CassandraClient;
import com.impetus.kundera.ejb.EntityManagerImpl;
import com.impetus.kundera.metadata.EntityMetadata;
import com.impetus.kundera.property.PropertyAccessException;
import com.impetus.kundera.property.PropertyAccessorFactory;
import com.impetus.kundera.property.PropertyAccessorHelper;
import com.impetus.kundera.proxy.EnhancedEntity;

/**
 * DataAccessor implementation for Cassandra's SuperColumnFamily.
 * 
 * @author animesh.kumar
 */
public final class SuperColumnFamilyDataAccessor extends
		BaseDataAccessor<SuperColumn> {

	/** The Constant log. */
	private static final Log log = LogFactory
			.getLog(SuperColumnFamilyDataAccessor.class);

	/** The Constant TO_ONE_SUPER_COL_NAME. */
	private static final String TO_ONE_SUPER_COL_NAME = "FKey-TO";

	/**
	 * Instantiates a new super column family data accessor.
	 * 
	 * @param em
	 *            the em
	 */
	public SuperColumnFamilyDataAccessor(EntityManagerImpl em) {
		super(em);
	}

	/*
	 * @see com.impetus.kundera.db.DataAccessor#read(java.lang.Class,
	 * com.impetus.kundera.metadata.EntityMetadata, java.lang.String)
	 */
	@Override
	public <E> E read(Class<E> clazz, EntityMetadata m, String id)
			throws Exception {
		log.debug("Cassandra >> Read >> " + clazz.getName() + "_" + id);

		String keyspace = m.getKeyspaceName();
		String family = m.getColumnFamilyName();

		// get super column names for this entity
		List<String> scNames = m.getSuperColumnFieldNames();
		scNames.add(TO_ONE_SUPER_COL_NAME);

		// load column from DB
		List<SuperColumn> columns = ((CassandraClient) getEntityManager().getClient())
				.loadSuperColumns(keyspace, family, id, // row id
						scNames.toArray(new String[0]) // array of names
				);

		E e;
		if (null == columns || columns.size() == 0) {
			e = null;
		} else {
		    e = fromThriftRow(clazz, m, this.new ThriftRow(id, family, columns));
		}

		return e;
	}

	/*
	 * @see com.impetus.kundera.db.DataAccessor#read(java.lang.Class,
	 * com.impetus.kundera.metadata.EntityMetadata, java.lang.String[])
	 */
	@Override
	public <E> List<E> read(Class<E> clazz, EntityMetadata m, String... ids)
			throws Exception {
		log.debug("Cassandra >> Read >> " + clazz.getName() + "_("
				+ Arrays.asList(ids) + ")");

		String keyspace = m.getKeyspaceName();
		String family = m.getColumnFamilyName();

		List<E> entities = new ArrayList<E>();

		// load columns from DB
		Map<String, List<SuperColumn>> map = ((CassandraClient)getEntityManager().getClient())
				.loadSuperColumns(keyspace, family, ids);

		// Iterate and populate entities
		for (Map.Entry<String, List<SuperColumn>> entry : map.entrySet()) {

			String id = entry.getKey();
			List<SuperColumn> columns = entry.getValue();

			if (entry.getValue().size() == 0) {
				log.debug("@Entity not found for id: " + id);
				continue;
			}

			E e = fromThriftRow(clazz, m, this.new ThriftRow(id, family,
					columns));
			entities.add(e);
		}

		return entities;
	}

	/*
	 * @seecom.impetus.kundera.db.DataAccessor#write(com.impetus.kundera.proxy.
	 * EnhancedEntity, com.impetus.kundera.metadata.EntityMetadata)
	 */
	@Override
	public void write(EnhancedEntity e, EntityMetadata m) throws Exception {
		String entityName = e.getEntity().getClass().getName();
		String id = e.getId();

		log.debug("Cassandra >> Write >> " + entityName + "_" + id);

		String keyspace = m.getKeyspaceName();
		String family = m.getColumnFamilyName();

		BaseDataAccessor<SuperColumn>.ThriftRow tf = toThriftRow(e, m);

		((CassandraClient)getEntityManager().getClient()).writeSuperColumns(keyspace, family, // columnFamily
				tf.getId(), // row id
				tf.getColumns().toArray(new SuperColumn[0]) // list of columns
				);
	}

	// Helper method to convert ThriftRow to @Entity
	/**
	 * From thrift row.
	 * 
	 * @param <E>
	 *            the element type
	 * @param clazz
	 *            the clazz
	 * @param m
	 *            the m
	 * @param cr
	 *            the cr
	 * @return the e
	 * @throws Exception
	 *             the exception
	 */
	private <E> E fromThriftRow(Class<E> clazz, EntityMetadata m,
			BaseDataAccessor<SuperColumn>.ThriftRow cr) throws Exception {

		// Instantiate a new instance
		E e = clazz.newInstance();

		// Set row-key. Note: @Id is always String.
		PropertyAccessorHelper.set(e, m.getIdProperty(), cr.getId());

		// Get a name->field map for super-columns
		Map<String, Field> columnNameToFieldMap = new HashMap<String, Field>();
		for (Map.Entry<String, EntityMetadata.SuperColumn> entry : m
				.getSuperColumnsMap().entrySet()) {
			for (EntityMetadata.Column cMetadata : entry.getValue()
					.getColumns()) {
				columnNameToFieldMap.put(cMetadata.getName(), cMetadata
						.getField());
			}
		}

		for (SuperColumn sc : cr.getColumns()) {

			String scName = PropertyAccessorFactory.STRING.fromBytes(sc
					.getName());
			boolean intoRelations = false;
			if (scName.equals(TO_ONE_SUPER_COL_NAME)) {
				intoRelations = true;
			}

			for (Column column : sc.getColumns()) {
				String name = PropertyAccessorFactory.STRING.fromBytes(column
						.getName());
				byte[] value = column.getValue();

				if (value == null) {
					continue;
				}

				if (intoRelations) {
					EntityMetadata.Relation relation = m.getRelation(name);

					String foreignKeys = PropertyAccessorFactory.STRING
							.fromBytes(value);
					Set<String> keys = deserializeKeys(foreignKeys);
					getEntityManager().getEntityResolver().populateForeignEntities(e, cr.getId(),
							relation, keys.toArray(new String[0]));

				} else {
					// set value of the field in the bean
					Field field = columnNameToFieldMap.get(name);
					PropertyAccessorHelper.set(e, field, value);
				}
			}
		}
		return e;
	}

	// Helper method to convert @Entity to ThriftRow
	/**
	 * To thrift row.
	 * 
	 * @param e
	 *            the e
	 * @param m
	 *            the m
	 * @return the base data accessor. thrift row
	 * @throws Exception
	 *             the exception
	 */
	private BaseDataAccessor<SuperColumn>.ThriftRow toThriftRow(
			EnhancedEntity e, EntityMetadata m) throws Exception {

		// timestamp to use in thrift column objects
		long timestamp = System.currentTimeMillis();

		BaseDataAccessor<SuperColumn>.ThriftRow cr = this.new ThriftRow();

		// column-family name
		cr.setColumnFamilyName(m.getColumnFamilyName());

		// Set row key
		cr.setId(e.getId());

		for (EntityMetadata.SuperColumn superColumn : m.getSuperColumnsAsList()) {
			String superColumnName = superColumn.getName();

			List<Column> columns = new ArrayList<Column>();

			for (EntityMetadata.Column column : superColumn.getColumns()) {
				Field field = column.getField();
				String name = column.getName();

				try {
					byte[] value = PropertyAccessorHelper.get(e.getEntity(),
							field);
					if (null != value) {
						columns.add(new Column(PropertyAccessorFactory.STRING
								.toBytes(name), value, timestamp));
					}
				} catch (PropertyAccessException exp) {
					log.warn(exp.getMessage());
				}
			}
			cr.addColumn(new SuperColumn(PropertyAccessorFactory.STRING
					.toBytes(superColumnName), columns));
		}

		// add toOne relations
		List<Column> columns = new ArrayList<Column>();
		for (Map.Entry<String, Set<String>> entry : e.getForeignKeysMap()
				.entrySet()) {
			String property = entry.getKey();
			Set<String> foreignKeys = entry.getValue();

			String keys = serializeKeys(foreignKeys);
			if (null != keys) {
				columns.add(new Column(PropertyAccessorFactory.STRING
						.toBytes(property), PropertyAccessorFactory.STRING
						.toBytes(keys), timestamp));
			}
		}
		if (!columns.isEmpty()) {
			cr.addColumn(new SuperColumn(PropertyAccessorFactory.STRING
					.toBytes(TO_ONE_SUPER_COL_NAME), columns));
		}
		return cr;
	}

}
